import axios from "axios";
import qs from "qs";
import {
    AuthClient,
    GrantTokenResult,
    LogoutHook,
    Message,
    OAuthTokenOptions,
    OnlineSessionInfo,
    RespPayload,
    SDKOptions,
    SSOLoginOptions,
    SSOLoginState,
    TokenEndpointResp
} from "./types";
import {createStore} from "./store";
import {getBrowserInfo} from "./utils";
import api from "./api";
import cookieUtil from "./cookie/api";

let sdkInstance: AuthClient;

export default class AuthClientSdk {

    public static create(sdkOptions: SDKOptions): AuthClient {
        if (sdkInstance) return sdkInstance;
        const {
            dev,
            clientId,
            clientSecret,
            requestTimeout,
            authServerIp,
            authServerPort,
            authServerSsl,
            loginRedirectUri,
            proxyPath,
            tokenPersistence,
            heartbeatIp,
            heartbeatPort,
            heartbeatPath,
            terminalType,
            logoutTogether,
            appCode
        } = sdkOptions

        const store = createStore(tokenPersistence)
        const sdkMultiplePageNotifyPageId = 'sdk-multiple-page-notify-page'
        const sdkMultiplePageNotifyPage = document.getElementById(sdkMultiplePageNotifyPageId) as HTMLIFrameElement || document.createElement("iframe");
        const sdkMultiplePageNotifyPageUrl = new URL(`http${authServerSsl ? 's' : ''}://${authServerIp}:${authServerPort}/sdk?hostOrigin=${btoa(window.location.origin)}`);
        sdkMultiplePageNotifyPage.id = sdkMultiplePageNotifyPageId
        sdkMultiplePageNotifyPage.src = sdkMultiplePageNotifyPageUrl.toString();
        sdkMultiplePageNotifyPage.style.zIndex = '-999'
        sdkMultiplePageNotifyPage.style.width = '0px'
        sdkMultiplePageNotifyPage.style.height = '0px'
        sdkMultiplePageNotifyPage.style.opacity = '0'
        sdkMultiplePageNotifyPage.style.position = 'absolute'
        sdkMultiplePageNotifyPage.style.pointerEvents = 'none'
        const checkMessage = (message: Message<any>) => {
            const {timestamp, type} = message;
            if (!timestamp || !type) {
                return false;
            }
            return new Date().getMilliseconds() - timestamp <= 3000;
        }
        const multiplePageNotify = (type: string, body?: any) => {
            const message: Message<any> = {
                type,
                body,
                timestamp: new Date().getTime()
            }
            sdkMultiplePageNotifyPage.contentWindow.postMessage(message, sdkMultiplePageNotifyPageUrl.origin)
        }
        const sdkMultiplePageNotifyPageMessageHandler = (event: MessageEvent) => {
            if (!checkMessage(event.data)) {
                return;
            }
            const message = event.data as Message<any>
            switch (message.type) {
                case 'account-logout':
                    logoutTogether && message.body.account_id === store.get("account_id") && doSsoLogout()
                    break;
                case 'sdk-page-complete':
                    console.info("account-page-complete", message)
                    break;
            }
        }
        window.addEventListener('message', sdkMultiplePageNotifyPageMessageHandler)
        document.getElementById(sdkMultiplePageNotifyPageId) || document.body.append(sdkMultiplePageNotifyPage);

        let logoutHook: LogoutHook;

        const authService = axios.create({
            baseURL: proxyPath,
            timeout: requestTimeout || 60 * 1000
        })

        authService.interceptors.request.use(config => {
            if (dev && clientSecret) {
                config.headers['Authorization'] = `Basic ${btoa(`${clientId}:${clientSecret}`)}`
            }
            return config;
        })

        let authorization_code: string | null = null;

        const doSsoLogout = async (redirectUri?: string) => {
            try {
                await offline()
            } finally {
                if (logoutHook) {
                    redirectUri = redirectUri || logoutHook(true) as string
                }
                store.clear()
                redirectUri = redirectUri || window.location.href
                const searchParams = new URLSearchParams();
                redirectUri && searchParams.append("redirect_uri", redirectUri)
                window.location.href = `http${authServerSsl ? 's' : ''}://${authServerIp}:${authServerPort}/sso/logout?${searchParams.toString()}`
            }
        }

        const setLogoutHook = (hook: LogoutHook): void => {
            logoutHook = hook
        }

        const ssoLogin = (ssoLoginOptions?: SSOLoginOptions): SSOLoginState => {
            const {
                state,
                loginPageCode
            } = ssoLoginOptions || {}

            const urlSearchParams = new URLSearchParams(window.location.search)
            const code = urlSearchParams.get("code")
            if (!code) {
                const searchParams = new URLSearchParams();
                searchParams.append("response_type", "code")
                searchParams.append("client_id", clientId)
                searchParams.append("redirect_uri", loginRedirectUri)
                if (state) {
                    searchParams.append("state", btoa(JSON.stringify(state)))
                }
                if (loginPageCode) {
                    searchParams.append("login_page_code", loginPageCode)
                }
                window.location.href = `http${authServerSsl ? 's' : ''}://${authServerIp}:${authServerPort}/oauth/authorize?${searchParams.toString()}`;
                return false;
            } else {
                const state = urlSearchParams.get("state")
                urlSearchParams.delete("state")
                urlSearchParams.delete("code")
                history.replaceState(null, null, window.location.href.replace(window.location.search, urlSearchParams.toString() ? `?${urlSearchParams.toString()}` : ''))
                authorization_code = code;
                if (state) {
                    return JSON.parse(atob(state))
                } else {
                    return true;
                }
            }
        }

        const ssoLogout = async (redirectUri?: string): Promise<void> => {
            multiplePageNotify("account-logout", {account_id: store.get("account_id")});
            await doSsoLogout(redirectUri)
        }

        const grantToken = async (oauthTokenOptions: OAuthTokenOptions): Promise<GrantTokenResult> => {
            let {grantType, parameters, getApiRespHeaders} = oauthTokenOptions
            if (grantType === 'authorization_code') {
                let {redirect_uri} = parameters || {}
                parameters = {
                    code: authorization_code,
                    redirect_uri: redirect_uri || loginRedirectUri
                }
            }
            const resp = await authService.request({
                method: 'post',
                url: api.tokenApi,
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                },
                data: qs.stringify({...parameters, grant_type: grantType})
            })
            if (!resp.data) {
                console.error("获取令牌接口调用失败，响应数据为空")
                throw new Error("获取令牌接口调用失败，响应数据为空");
            }
            const respHeaders: Record<string, string> = {};
            if (getApiRespHeaders) {
                getApiRespHeaders.forEach(header => {
                    respHeaders[header] = resp.headers[header]
                })
            }
            if (resp.data.error) {
                throw {result: resp.data, respHeaders}
            }
            if (resp.data.access_token) {
                store.save("access_token", resp.data.access_token)
                store.save("refresh_token", resp.data.refresh_token)
                store.save("account_id", resp.data.accountId)
                online()
                return {result: resp.data as TokenEndpointResp, respHeaders}
            }
            throw new Error("意外的响应格式" + JSON.stringify(resp.data));
        }

        const accessToken = (): string => {
            return store.get("access_token")
        }

        const refreshToken = () => {
            const refreshToken = store.get("refresh_token");
            if (!refreshToken) {
                return Promise.reject("无刷新令牌")
            }
            return grantToken({grantType: "refresh_token", parameters: {refresh_token: refreshToken}})
        }

        const enableHeartbeat = !!((heartbeatIp && heartbeatPort) || heartbeatPath)
        let heartbeatAxios = undefined;
        if (enableHeartbeat) {
            const heartbeatUrl = heartbeatPath ? `http${authServerSsl ? 's' : ''}://${authServerIp}:${authServerPort}${heartbeatPath}`:`http${authServerSsl ? 's' : ''}://${heartbeatIp}:${heartbeatPort}`;
            heartbeatAxios = axios.create({
                baseURL: heartbeatUrl,
                timeout: requestTimeout || 60 * 1000
            })

            heartbeatAxios.interceptors.request.use(config => {
                if (dev && clientSecret) {
                    config.headers['Authorization'] = `Bearer ${accessToken()}`
                }
                return config;
            })

            heartbeatAxios.interceptors.response.use(
                resp => {
                    if (resp.data.code === 200) {
                        return resp.data
                    } else {
                        return Promise.reject(resp.data)
                    }
                },
                async error => {
                    const originalRequest = error.config;
                    if (error?.response?.status === 401 && !originalRequest._retry) {
                        originalRequest._retry = true;
                        try {
                            await refreshToken()
                            return heartbeatAxios(originalRequest);
                        } catch (err) {
                            console.error(err)
                            return Promise.reject(error);
                        }
                    }
                    return Promise.reject(error);
                }
            )

        }
        let isOnline = false
        let loopTimer = undefined;
        const explorerInfo = getBrowserInfo();
        const terminal = `${explorerInfo.name}/${explorerInfo.version}`
        const connectionOnlineSession = async () => {
            const connectionParams: Record<string, any> = {
                terminalType, appCode, terminal
            }
            const saveId = cookieUtil.get("account_sdk_aosid")
            if (saveId) {
                connectionParams.id = saveId
            }
            const resp: RespPayload<OnlineSessionInfo> = await heartbeatAxios?.request({
                method: "get",
                url: api.accountOnlineSessionConnectionApi,
                params: connectionParams
            });
            cookieUtil.set("account_sdk_aosid", resp.data.id, {path: "/"})
            return resp.data
        }
        const activeOnlineSession = async () => {
            const resp: RespPayload<boolean> = await heartbeatAxios?.request({
                method: "get",
                url: api.accountOnlineSessionActiveApi,
                params: {
                    terminalType, appCode, terminal, id: cookieUtil.get("account_sdk_aosid")
                }
            });
            return resp.data
        }
        const disconnectionOnlineSession = async () => {
            const resp: RespPayload<boolean> = await heartbeatAxios?.request({
                method: "delete",
                timeout: 1000,
                url: api.accountOnlineSessionDisconnectionApi,
                params: {
                    terminalType, appCode, terminal, id: cookieUtil.get("account_sdk_aosid")
                }
            });
            return resp.data
        }

        const startOnlineSessionKeepalive = async () => {
            try {
                const {heartbeatInterval} = await connectionOnlineSession()
                loopTimer && clearTimeout(loopTimer)
                const setHeartBeatTimer = () => {
                    loopTimer = setTimeout(async () => {
                        try {
                            const status = await activeOnlineSession()
                            if (status) {
                                setHeartBeatTimer();
                            } else {
                                startOnlineSessionKeepalive()
                            }
                        } catch (err) {
                            startOnlineSessionKeepalive()
                        }
                    }, (Math.floor(Math.random() * 2.5) + 1) * 1000 + heartbeatInterval)
                }
                setHeartBeatTimer()
            } catch (err) {
                loopTimer = setTimeout(async () => {
                    await startOnlineSessionKeepalive()
                }, 15 * 1000)
            }
        }

        const online = () => {
            isOnline = true
            if (enableHeartbeat) {
                startOnlineSessionKeepalive()
            }
        }

        const offline = async () => {
            isOnline = false
            if (loopTimer) {
                clearTimeout(loopTimer)
            }
            if (enableHeartbeat) {
                await disconnectionOnlineSession()
            }

        }

        sdkInstance = {
            setLogoutHook,
            ssoLogin,
            ssoLogout,
            grantToken,
            accessToken,
            refreshToken
        }
        return sdkInstance;
    }
}
